/*******************************************************************************
 * Copyright (c) 2007, Geophysical Center RAS
 * Sergey Berezin, Dmitry Voitsekhovskiy
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * - Neither the name of the Geophysical Center RAS nor the names
 * of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using VisualEsse.DataEngine.Types;
using System.ComponentModel;
using System.IO;

namespace VisualEsse.DataEngine
{
    /// <summary>
    /// Retrieves data.
    /// </summary>
    public interface IDataSource
    {
        /// <summary>
        /// Retrieves the data.
        /// </summary>
        object GetData();
    }


    /// <summary>
    /// Retrives two-dimensional grid data.
    /// </summary>
    public interface IGrid2dDataSource : IDataSource
    {
        /// <summary>
        /// Retrieves the grid related to the data.
        /// </summary>
        /// <returns></returns>
        IGrid2d GetGrid();

        GeoRect DataDefinitionRectangle { get;}

        /// <summary>
        /// Retrives the data with specified detail level.
        /// </summary>
        /// <exception cref="VisualEsse.DataEngine.DataIsNotAvailableException">
        /// Throws the VisualEsse.DataEngine.DataIsNotAvailableException exception if
        /// the requested region is not available.
        /// </exception>
        object GetData(GeoRect region);
    }

    public class DataIsNotAvailableException : Exception
    {
        public DataIsNotAvailableException() : base()
        {

        }

        public DataIsNotAvailableException(string message) : base(message)
        {

        }
    }

    public abstract class DataSource : IDisposable
    {
        protected double latMin;
        protected double latMax;
        protected double lonMin;
        protected double lonMax;

        protected double latStep = 2.5;
        protected double lonStep = 2.5;

        protected DateTime date;
        protected DataProvider provider;

        public DataSource(DataProvider provider, GeoPoint min, GeoPoint max, GeoPoint step, DateTime when)
        {
            this.provider = provider;
            lonMin = min.Longitude;
            lonMax = max.Longitude;
            latMin = min.Latitude;
            latMax = max.Latitude;
            lonStep = provider.GridStep.Longitude;
            latStep = provider.GridStep.Latitude;
            date = when;
            //latStep = step.Latitude;
            //lonStep = step.Longitude;
        }

        [Description("Minimal latitude value that specifies lower boundary of the explored region."),
        Category("Explored region")]
        public double LatitudeMin
        {
            get
            {
                return latMin;
            }
            set
            {
                if (value >= latMax)
                    throw new Exception("Minimal value must be less than maximal");
                latMin = value;
            }
        }

        [Description("Maximal latitude value that specifies upper boundary of the explored region."),
        Category("Explored region")]
        public double LatitudeMax
        {
            get
            {
                return latMax;
            }
            set
            {
                if (value <= latMin)
                    throw new Exception("Minimal value must be less than maximal");
                latMax = value;
            }
        }

        [Description("Minimal longitude value that specifies left boundary of the explored region."),
        Category("Explored region")]
        public double LongitudeMin
        {
            get
            {
                return lonMin;
            }
            set
            {
                if (value >= lonMax)
                    throw new Exception("Minimal value must be less than maximal");
                lonMin = value;
            }
        }

        [Description("Maximal longitude value that specifies right boundary of the explored region."),
        Category("Explored region")]
        public double LongitudeMax
        {
            get
            {
                return lonMax;
            }
            set
            {
                if (value <= lonMin)
                    throw new Exception("Minimal value must be less than maximal");
                lonMax = value;
            }
        }

        [Browsable(false)]
        public GeoRect DataDefinitionRectangle
        {
            get
            {
                return new GeoRect((float)lonMin, (float)lonMax, (float)latMin, (float)latMax);
            }
        }

        [DisplayName("Current date/time"),
        Description("Current date and time."),
        Category("Explored region")]
        public DateTime CurrentDateTime
        {
            get
            {
                return date;
            }
            set
            {
                if (value.Year < 1973)
                    throw new Exception("Wrong date");
                date = value;
            }
        }

        #region IDisposable Members

        public virtual void Dispose()
        {
        }

        #endregion
    }

    public abstract class EsseBaseDataSource : DataSource
    {
        protected EsseParameterDescription parameter;

        public EsseBaseDataSource(DataProvider provider, EsseParameterDescription parameter, GeoPoint min, GeoPoint max, DateTime date)
            : base(provider, min, max, provider.GridStep, date)
        {
            this.parameter = parameter;
        }


        #region IGrid2dDataSource Members

        public IGrid2d GetGrid()
        {
            return new UniformGrid2d(
                new UniformGrid1d((int)((lonMax - lonMin) / (lonStep)) + 1, lonMin, lonStep),
                new UniformGrid1d((int)((latMax - latMin) / (latStep)) + 1, latMin, latStep));
        }

        public object GetData(GeoRect region)
        {
            // 1. We have to check whether requested region has intersection with
            //    our rectangle or not.
            GeoRect rect = DataDefinitionRectangle;
            GeoRect[] rects = GeoRect.Intersect(rect, region);
            if (rects.Length == 0)
                throw new VisualEsse.DataEngine.DataIsNotAvailableException();

            // 2. Requesting the data.
            GeoRect[] simpleRects = rects[0].GetSimpleRects();
            GeoUniformScalarField2d sf1, sf2 = null;
            sf1 = (GeoUniformScalarField2d)GetData(parameter, simpleRects[0], date);

            // If we have to divide grid in two grids 
            // since we have passed through 180 degrees by longitude.
            if (simpleRects.Length > 1)
            {
                // Have to combine two fields
                sf2 = (GeoUniformScalarField2d)GetData(parameter, simpleRects[1], date);

                GeoUniformGrid2d grid = new GeoUniformGrid2d(
                    new UniformGrid1d(sf1.GeoGrid.Width + sf2.GeoGrid.Width - 1, sf1.GeoGrid.Longitudes.Origin, sf1.GeoGrid.LongStep),
                    sf1.GeoGrid.Y);
                ScalarArray2d data = new ScalarArray2d(grid.Width, grid.Height);

                double[,] a = data.Data;
                for (int i = 0; i < sf1.GeoGrid.Width - 1; i++)
                    for (int j = 0; j < grid.Height; j++)
                        a[i, j] = sf1.Data[i, j];

                for (int i = 0, k = sf1.GeoGrid.Width - 1; i < sf2.GeoGrid.Width; i++, k++)
                    for (int j = 0; j < grid.Height; j++)
                        a[k, j] = sf2.Data[i, j];

                GeoUniformScalarField2d sf = new GeoUniformScalarField2d(grid, data);
                return sf;
            }
            else
            {
                return sf1;
            }
        }

        protected abstract IScalarArray2d[] GetArraysFromSource(EsseWebServiceDataProvider esseProvider, EsseParameterDescription parameter,
            float lat0, float lat1, int latPoints, float lon0, float lon1, int lonPoints, DateTime dateTime, int stepInMin);

        protected virtual object GetData(EsseParameterDescription parameter, GeoRect region, DateTime date)
        {
            DateTime dayStart = new DateTime(date.Year, date.Month, date.Day, 0, 0, 0);
            ArrayStorage storage = null;
            EsseWebServiceDataProvider esseProvider = (EsseWebServiceDataProvider)provider;

            try
            {
                List<Rectangle> rects = null;
                GeoPoint gmin = provider.GlobalMin;
                GeoPoint gmax = provider.GlobalMax;

                int width = (int)(GeoPoint.GetWidthLongitude(gmin, gmax) / provider.GridStep.Longitude);
                int height = (int)(GeoPoint.GetHeightLatitude(gmin, gmax) / provider.GridStep.Latitude);

                //--------------------------------------------------------
                // Getting indices for requested region
                float latDist, lonDist;
                GeoPoint.CircleDistance(gmin,
                    region.LowerLeft, out latDist, out lonDist);
                Point glIndices = new Point();
                glIndices.X = (int)Math.Ceiling(lonDist / lonStep);
                glIndices.Y = (int)Math.Ceiling(latDist / latStep);

                GeoPoint.CircleDistance(gmin,
                    region.UpperRight, out latDist, out lonDist);
                if (lonDist == 0) // from -180 to 180
                    lonDist = 360.0f - (float)lonStep;
                Point glIndicesEnd = new Point();
                glIndicesEnd.X = (int)Math.Floor(lonDist / lonStep);
                glIndicesEnd.Y = (int)Math.Floor(latDist / latStep);

                int latCount = glIndicesEnd.Y - glIndices.Y + 1;
                int lonCount = glIndicesEnd.X - glIndices.X + 1;
                //--------------------------------------------------------

                //--------------------------------------------------------
                // Searching for the requested data in the cache.
                ICache cache = (provider as EsseWebServiceDataProvider).Cache;

                // Opening array storage for READING
                CacheKey key = GetCacheKey(date, parameter);
                Stream stream = cache.OpenFile(key, FileAccess.Read);
                if (stream != null)
                {
                    try
                    {
                        // If file exists it may contain what we need!
                        storage = new ArrayStorage(stream, width, height);
                    }
                    catch
                    {
                        if (storage != null)
                        {
                            storage.Dispose();
                        }
                        stream = null;
                    }

                    if (stream != null)
                    {
                        // Checking whether we have entire data set or not?
                        rects = storage.GetUnpresentedBoundingBoxes(
                            glIndices.X, glIndices.Y, lonCount, latCount);
                        if (rects != null)
                            storage.Dispose(); // Shall create new storage for WRITING in this case.
                    }
                }

                // Creating new storage
                if (stream == null) // Storage either doesn't exist or is corrupted.
                {
                    stream = cache.CreateFile(key, FileAccess.ReadWrite);
                    storage = new ArrayStorage(stream, width, height);

                    rects = storage.GetUnpresentedBoundingBoxes(
                            glIndices.X, glIndices.Y, lonCount, latCount);
                }
                else if (rects != null) // Storage exists, but data is not presented.
                {
                    stream = cache.OpenFile(key, FileAccess.ReadWrite);
                    storage = new ArrayStorage(stream, width, height);
                }

                //--------------------------------------------------------
                // Loading missed parts.
                if (rects != null)
                {
                    int stepInMin = (int)provider.TimeStep.TotalMinutes;

                    // We have to open other storages at now!
                    int total = 24 / provider.TimeStep.Hours;
                    ArrayStorage[] storages = new ArrayStorage[total];

                    DateTime current = dayStart;
                    int iRequested = 0;
                    for (int i = 0; i < total; i++)
                    {
                        if (current != date)
                        {
                            key = GetCacheKey(current, parameter);
                            stream = cache.OpenOrCreateFile(key, FileAccess.ReadWrite);
                            storages[i] = new ArrayStorage(stream, width, height);
                        }
                        else
                        {
                            storages[iRequested = i] = storage;
                        }

                        current += provider.TimeStep;
                    }

                    try
                    {
                        // Not the entire data is presented in the storage.
                        // We have to load & store unpresented parts.
                        foreach (Rectangle rect in rects)
                        {
                            PointF p = GlobalIndicesToLatLon(rect.X, rect.Y);
                            PointF pe = GlobalIndicesToLatLon(rect.X + rect.Width - 1, rect.Y + rect.Height - 1);

                            ScalarArray2d[] data = (ScalarArray2d[])GetArraysFromSource(esseProvider, parameter, p.Y, pe.Y, rect.Height, p.X, pe.X, rect.Width, dayStart, stepInMin);
                                
                                /*EsseDataSource.GetDataArrayForTheDay(
                                esseProvider.DataServiceUrl, esseProvider.ResourceName, esseProvider.DataSourceString,
                                parameter, p.Y, pe.Y, rect.Height, p.X, pe.X, rect.Width, dayStart, stepInMin);*/

                            for (int i = 0; i < total; i++)
                            {
                                storages[i].SaveData(rect.X, rect.Y, data[i].Data);
                            }
                        }
                    }
                    finally
                    {
                        for (int i = 0; i < total; i++)
                            if (i != iRequested)
                                storages[i].Dispose();
                    }
                }

                double[,] stripe = null;
                if (region.UpperRight.Longitude == -180)
                {
                    try
                    { // TODO: ó÷èòûâàòü ïîëîñó äëÿ 180 ãðàäóñîâ âíóòðè ArrayStorage
                        // ÍÅ òðåáîâàòü äåëåíèÿ íàöåëî.                            
                        stripe = storage.LoadData(0, glIndices.Y, 1, latCount);
                    }
                    catch
                    { }
                }

                // Now the entire array must be in the storage.
                double[,] array = storage.LoadData(glIndices.X, glIndices.Y, lonCount, latCount);
                if (stripe != null)
                {
                    double[,] a2 = new double[array.GetLength(0) + 1, array.GetLength(1)];
                    for (int i = 0; i < array.GetLength(0); i++)
                        for (int j = 0; j < array.GetLength(1); j++)
                            a2[i, j] = array[i, j];
                    int last = array.GetLength(0);
                    for (int j = 0; j < array.GetLength(1); j++)
                        a2[last, j] = stripe[0, j];
                    array = a2;
                }

                float currLonMin = (float)(gmin.Longitude + glIndices.X * lonStep);
                float currLatMin = (float)(gmin.Latitude + glIndices.Y * latStep);

                GeoUniformGrid2d grid = new GeoUniformGrid2d(
                    new UniformGrid1d(array.GetLength(0), currLonMin, lonStep),
                    new UniformGrid1d(array.GetLength(1), currLatMin, latStep));

                return new GeoUniformScalarField2d(grid, new ScalarArray2d(array));
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
            finally
            {
                if (storage != null)
                    storage.Dispose();
            }
        }
        
        protected PointF GlobalIndicesToLatLon(int i, int j)
        {
            GeoPoint min = provider.GlobalMin;
            double lon = min.Longitude + i * lonStep;
            double lat = min.Latitude + j * latStep;

            return new PointF((float)lon, (float)lat);
        }

        protected Point LatLonToGlobalIndices(double lat, double lon)
        {
            GeoPoint min = provider.GlobalMin;
            int i = (int)Math.Floor((lon - min.Longitude) / lonStep);
            int j = (int)Math.Floor((lat - min.Latitude) / latStep);

            return new Point(i, j);
        }

        protected CacheKey GetCacheKey(DateTime date, EsseParameterDescription parameter)
        {
            return EsseParametersCache.CreateKey(parameter, date);
        }

        #endregion

        #region IDataSource Members

        public object GetData()
        {
            return GetData(parameter, DataDefinitionRectangle, date);
        }

        #endregion
    }
}
